"""
消息监控模块
- 统一封装窗口截图、消息框检测、OCR 识别、调试预览与监听回调。
- 核心对外接口：MessageMonitor 类。
- 内部辅助函数按功能分组：截图/检测/预览/OCR。
"""
import datetime
import threading
import time
from typing import List, Tuple, Optional, Callable

import pytesseract
import win32con
import win32gui
import win32ui
from PIL import Image, ImageDraw, ImageFilter

from window_manager import WindowManager


def _compute_abs_rect(hwnd, rel_rect: Tuple[float, float, float, float]) -> Optional[Tuple[int, int, int, int]]:
    client_rect = WindowManager.get_client_rect_on_screen(hwnd)
    if not client_rect:
        return None
    c_left, c_top, c_right, c_bottom = client_rect
    cw = max(0, c_right - c_left)
    ch = max(0, c_bottom - c_top)
    if cw == 0 or ch == 0:
        return None
    x1 = int(c_left + cw * rel_rect[0])
    y1 = int(c_top + ch * rel_rect[1])
    x2 = int(c_left + cw * rel_rect[2])
    y2 = int(c_top + ch * rel_rect[3])
    return x1, y1, x2, y2


def _capture_region_image(hwnd, rel_rect: Tuple[float, float, float, float]) -> Optional[Image.Image]:
    abs_rect = _compute_abs_rect(hwnd, rel_rect)
    if not abs_rect:
        return None
    x1, y1, x2, y2 = abs_rect
    width = max(1, x2 - x1)
    height = max(1, y2 - y1)

    hwndDC = win32gui.GetWindowDC(0)
    mfcDC = win32ui.CreateDCFromHandle(hwndDC)
    saveDC = mfcDC.CreateCompatibleDC()
    saveBitMap = win32ui.CreateBitmap()
    saveBitMap.CreateCompatibleBitmap(mfcDC, width, height)
    saveDC.SelectObject(saveBitMap)
    try:
        saveDC.BitBlt((0, 0), (width, height), mfcDC, (x1, y1), win32con.SRCCOPY)
        bmpinfo = saveBitMap.GetInfo()
        bmpstr = saveBitMap.GetBitmapBits(True)
        img = Image.frombuffer(
            "RGB",
            (bmpinfo["bmWidth"], bmpinfo["bmHeight"]),
            bmpstr, "raw", "BGRX", 0, 1
        )
        return img
    finally:
        win32gui.DeleteObject(saveBitMap.GetHandle())
        saveDC.DeleteDC()
        mfcDC.DeleteDC()
        win32gui.ReleaseDC(0, hwndDC)


def _white_box_mask(img_rgb: Image.Image,
                    s_max: int = 40, v_min: int = 200) -> List[List[bool]]:
    """
    生成近白色掩码：S 低、V 高。
    返回二维布尔数组 mask[y][x]
    """
    hsv = img_rgb.convert("HSV")
    H, S, V = hsv.split()
    w, h = img_rgb.size
    ms = S.load()
    mv = V.load()
    mask = [[False] * w for _ in range(h)]
    for y in range(h):
        row = mask[y]
        for x in range(w):
            if ms[x, y] <= s_max and mv[x, y] >= v_min:
                row[x] = True
    return mask


def _connected_components(mask: List[List[bool]]) -> List[Tuple[int, int, int, int, int]]:
    """
    对布尔掩码做连通域，返回 [(minx,miny,maxx,maxy,area)]
    使用4邻域 BFS，纯 Python 实现避免额外依赖。
    """
    h = len(mask)
    if h == 0:
        return []
    w = len(mask[0])
    visited = [[False] * w for _ in range(h)]
    comps = []
    for y in range(h):
        for x in range(w):
            if mask[y][x] and not visited[y][x]:
                q = [(x, y)]
                visited[y][x] = True
                minx = maxx = x
                miny = maxy = y
                area = 0
                qi = 0
                while qi < len(q):
                    cx, cy = q[qi]
                    qi += 1
                    area += 1
                    if cx < minx: minx = cx
                    if cx > maxx: maxx = cx
                    if cy < miny: miny = cy
                    if cy > maxy: maxy = cy
                    # 4邻域
                    for nx, ny in ((cx - 1, cy), (cx + 1, cy), (cx, cy - 1), (cx, cy + 1)):
                        if 0 <= nx < w and 0 <= ny < h and mask[ny][nx] and not visited[ny][nx]:
                            visited[ny][nx] = True
                            q.append((nx, ny))
                comps.append((minx, miny, maxx, maxy, area))
    return comps


def _color_box_mask(img_rgb: Image.Image,
                    target_rgb: Tuple[int, int, int] = (226, 226, 224),
                    tolerance: int = 10) -> List[List[bool]]:
    """
    生成指定颜色掩码：检测与目标RGB颜色相近的像素
    target_rgb: 目标RGB颜色值
    tolerance: 容差范围，每个颜色通道的允许偏差
    返回二维布尔数组 mask[y][x]
    """
    w, h = img_rgb.size
    pixels = img_rgb.load()
    mask = [[False] * w for _ in range(h)]

    target_r, target_g, target_b = target_rgb

    for y in range(h):
        row = mask[y]
        for x in range(w):
            r, g, b = pixels[x, y][:3]  # 取RGB三个通道
            # 检查每个通道是否在容差范围内
            if (abs(r - target_r) <= tolerance and
                    abs(g - target_g) <= tolerance and
                    abs(b - target_b) <= tolerance):
                row[x] = True
    return mask


def _detect_message_boxes(img_rgb: Image.Image,
                          min_fill_ratio: float = 0.6,
                          gap_px: int = 6,
                          target_rgb: Tuple[int, int, int] = (226, 226, 224),
                          tolerance: int = 10) -> List[Tuple[int, int, int, int]]:
    """
    检测指定颜色的消息框：
    - 指定RGB颜色掩码连通域；
    - 面积占比（填充率）足够高；
    - 框上下均有 gap_px 的非指定颜色空隙，即为"完整消息框"。
    返回裁剪框列表 [(x1,y1,x2,y2)]，按 y 升序。
    """
    w, h = img_rgb.size
    mask = _color_box_mask(img_rgb, target_rgb=target_rgb, tolerance=tolerance)
    comps = _connected_components(mask)
    boxes = []
    for minx, miny, maxx, maxy, area in comps:
        bw = maxx - minx + 1
        bh = maxy - miny + 1
        fill = area / float(bw * bh)
        if fill < min_fill_ratio:
            continue

        # 上下空隙判定：取 bbox 上/下各 gap_px 带，如果越界或存在大量指定颜色像素则判为非完整
        def band_color_ratio(y_from: int, y_to: int) -> float:
            y_from = max(0, y_from)
            y_to = min(h - 1, y_to)
            if y_to < y_from:
                return 1.0  # 视为没有空隙
            color_pixels = 0
            total = (y_to - y_from + 1) * bw
            for yy in range(y_from, y_to + 1):
                row = mask[yy]
                for xx in range(minx, maxx + 1):
                    if row[xx]:
                        color_pixels += 1
            return color_pixels / float(total) if total > 0 else 1.0

        top_ratio = band_color_ratio(miny - gap_px, miny - 1)
        bot_ratio = band_color_ratio(maxy + 1, maxy + gap_px)
        # 需要上下带"非指定颜色"为主，这里用指定颜色像素占比 < 0.3 作为空隙条件
        if top_ratio < 0.3 and bot_ratio < 0.3:
            # 适度内缩，避免边缘描边影响 OCR
            shrink = 2
            x1 = max(0, minx + shrink)
            y1 = max(0, miny + shrink)
            x2 = min(w, maxx - shrink + 1)
            y2 = min(h, maxy - shrink + 1)
            if x2 > x1 and y2 > y1:
                boxes.append((x1, y1, x2, y2))
    boxes.sort(key=lambda b: b[1])
    return boxes


def _draw_boxes_on_image(img: Image.Image, boxes: List[Tuple[int, int, int, int]]) -> Image.Image:
    vis = img.copy()
    dr = ImageDraw.Draw(vis)
    for (x1, y1, x2, y2) in boxes:
        dr.rectangle((x1, y1, x2, y2), outline=(255, 0, 0), width=2)
    return vis


def _stack_images_vertical(images: List[Image.Image], max_width: int = 600) -> Image.Image:
    if not images:
        return None
    # 等比缩放到统一宽度
    scaled = []
    target_w = min(max_width, max(img.width for img in images))
    for img in images:
        if img.width != target_w:
            scale = target_w / img.width
            img = img.resize((target_w, max(1, int(img.height * scale))), Image.LANCZOS)
        scaled.append(img)
    total_h = sum(img.height for img in scaled)
    canvas = Image.new("L", (target_w, total_h), 255) if scaled[0].mode == "L" else Image.new("RGB",
                                                                                              (target_w, total_h),
                                                                                              (255, 255, 255))
    y = 0
    for img in scaled:
        canvas.paste(img, (0, y))
        y += img.height
    return canvas


def _ocr_single_box(
        ocr_img: Image.Image,
        ocr_lang: str,
) -> Tuple[str, str]:
    """
    识别单个消息框：
    - 原始分辨率清理（不缩放）
    - 灰度 + 二值化（不缩放）
    返回: (name, message, ocr_img, cleaned_img)

    解析规则：
    - 第一行 = 名称
    - 第二行及之后 = 内容
    """
    # 4) OCR
    text = pytesseract.image_to_string(ocr_img, lang=ocr_lang)
    lines = [ln.strip() for ln in text.splitlines() if ln.strip()]

    # 5) 解析 name / message
    name, message = "", ""
    if lines:
        name = lines[0]

        # 后续所有行合并成消息
        contents: List[str] = []
        for idx, raw in enumerate(lines[1:], start=1):
            contents.append(raw)

        message = "".join([s for s in contents if s])

    return name, message


# ------------ 主监控类：改为“按框识别” ------------

class MessageMonitor:
    """
    消息监控器
    - 负责从指定窗口定时截图、检测消息框、OCR 识别、关键词匹配与去重。
    - 提供监听回调以便 UI 或控制台实时刷新。
    - 可选调试预览，显示：原图+框、清理后的图、二值化 OCR 图 三阶段画面。
    使用方法概览：
    1) monitor = MessageMonitor(window_title="星痕共鸣", keywords=["娜宝", "粉猪"], ...)
    2) monitor.add_listener(lambda now, pairs, hits: ...)
    3) monitor.start() / monitor.stop() / monitor.toggle()
    线程模型：
    - start() 后会启动一个后台线程，循环执行抓取与识别，不阻塞主线程。
    去重策略：
    - 将每帧得到的 (name, msg) 对规范化为元组签名，与上一帧对比，仅在变化时通知监听器。
    """

    def __init__(
            self,
            window_title: str,
            keywords: List[str],
            rel_rect=(0.0, 0.65, 0.55, 1.5),
            interval_sec=1.0,
            ocr_lang="chi_sim",
            # 调试
            debug_preview: bool = False,
    ):
        self.window_title = window_title
        self.keywords = [k.lower() for k in keywords if k]
        self.rel_rect = rel_rect
        self.interval_sec = interval_sec
        self.ocr_lang = ocr_lang
        self._running = False
        self._thread = None

        self.debug_preview = debug_preview

        # 调试预览：保存最近一帧预览图，供 Web 界面获取
        import threading as _threading  # 局部导入避免顶层依赖
        self._last_preview_lock = _threading.Lock()
        self._last_raw_image: Optional[Image.Image] = None
        self._last_cleaned_image: Optional[Image.Image] = None

        # 打印去重缓存
        self._last_print_block: str = ""
        self._last_print_pair: Tuple[str, str] = ("", "")

        # 框检测参数（可按需暴露）
        self.box_min_fill = 0.6
        self.box_gap_px = 6
        # 修改：使用RGB颜色检测而非HSV
        self.target_rgb = (226, 226, 224)  # 目标RGB颜色
        self.color_tolerance = 10  # 颜色容差

        # 新增：监听器列表（用于 UI 实时刷新）
        self._listeners: List[Callable[[str, List[Tuple[str, str]], List[Tuple[str, str]]], None]] = []

        # 新增：上一帧签名，用于去重
        self._last_frame_signature: Optional[Tuple[Tuple[str, str], ...]] = None

    # 新增：注册/注销监听器
    def add_listener(self, listener: Callable[[str, List[Tuple[str, str]], List[Tuple[str, str]]], None]):
        """
        listener(now, pairs, hits)
        now: 'YYYY-MM-DD HH:MM:SS'
        pairs: 本次帧识别出的所有 (name, msg)
        hits: 本次帧命中的 (name, msg)，按 keywords 过滤
        """
        if callable(listener):
            self._listeners.append(listener)

    def remove_listener(self, listener):
        try:
            self._listeners.remove(listener)
        except ValueError:
            pass

    def _notify_listeners(self, now: str, pairs: List[Tuple[str, str]], hits: List[Tuple[str, str]]):
        for fn in list(self._listeners):
            try:
                fn(now, pairs, hits)
            except Exception:
                # UI 回调异常不影响监控主循环
                pass

    def start(self):
        """启动监控线程并（可选）启动调试预览窗口"""
        if self._running:
            return

        self._running = True
        self._thread = threading.Thread(target=self._loop, daemon=True)
        self._thread.start()
        print("[消息监控] 已启动")

    def stop(self):
        """停止监控线程并关闭调试预览窗口，重置去重状态"""
        if not self._running:
            return
        self._running = False
        # 预览窗口改为前端集成，这里不再管理本地窗口
        # 清理最近预览
        try:
            with self._last_preview_lock:
                self._last_raw_image = None
                self._last_cleaned_image = None
        except Exception:
            pass
        print("[消息监控] 已停止")
        # 重置上一帧签名，避免下次启动延续旧状态
        self._last_frame_signature = None

    def toggle(self):
        """切换监控开/关"""
        if self._running:
            self.stop()
        else:
            self.start()

    def enable_debug_preview(self):
        """启用调试预览（仅用于前端预览，不再弹出本地窗口）"""
        self.debug_preview = True

    def disable_debug_preview(self):
        """关闭调试预览（前端预览）"""
        self.debug_preview = False
        # 关闭调试时同时清空最近一帧的 Web 预览，避免显示陈旧画面
        try:
            with self._last_preview_lock:
                self._last_raw_image = None
                self._last_cleaned_image = None
        except Exception:
            pass

    def toggle_debug_preview(self):
        """切换调试预览开/关"""
        if self.debug_preview:
            self.disable_debug_preview()
            print("[调试预览] 已关闭")
        else:
            self.enable_debug_preview()
            print("[调试预览] 已开启")

    def _loop(self):
        """
        消息监控的主循环方法
        负责持续捕获窗口图像、检测消息框、进行OCR识别、关键词匹配等核心功能
        """
        hwnd = WindowManager.get_window_handle(self.window_title)

        while self._running:  # 主循环，直到停止标志被设置
            try:
                # ==================== 第1步：图像捕获 ====================
                # 捕获指定窗口区域的原始图像
                img_raw = _capture_region_image(hwnd, self.rel_rect)
                if img_raw is None:
                    time.sleep(self.interval_sec)  # 图像捕获失败，等待后重试
                    continue

                # ==================== 第2步：消息框检测 ====================
                # 使用RGB颜色检测图像中的"完整消息框"区域
                # 根据填充比例、间隙像素和指定RGB颜色进行检测
                boxes = _detect_message_boxes(
                    img_raw,
                    min_fill_ratio=self.box_min_fill,  # 最小填充比例
                    gap_px=self.box_gap_px,  # 间隙像素
                    target_rgb=self.target_rgb,  # 目标RGB颜色
                    tolerance=self.color_tolerance  # 颜色容差
                )

                # 为了提高处理效率和准确性，只处理最下面（最新）的消息框
                if boxes:
                    boxes = [boxes[-1]]  # 只保留最后一个（最下面）的消息框

                # ==================== 第3步：OCR文字识别准备 ====================
                # 初始化用于存储OCR结果和图像预览的容器
                pairs: List[Tuple[str, str]] = []  # 存储(姓名, 消息)元组对
                cleaned_previews: List[Image.Image] = []  # 存储清理后的图像预览

                # ==================== 第4步：逐框OCR识别 ====================
                # 对每个检测到的消息框进行文字识别
                for (bx1, by1, bx2, by2) in boxes:
                    ocr_img = img_raw.crop((bx1, by1, bx2, by2))

                    # 创建一个30x30的RGB (226,226,224)色块
                    color_block = Image.new("RGB", (31, 30), (226, 226, 224))
                    # 粘贴到cleaned_img的左上角
                    ocr_img.paste(color_block, (0, 0))

                    # 去掉频道标识
                    color_block = Image.new("RGB", (70, 30), (226, 226, 224))
                    ocr_img.paste(color_block, (0, 30))

                    ocr_img = ocr_img.filter(ImageFilter.SHARPEN)

                    # 对裁剪图像进行OCR识别，获取姓名、消息内容和处理后的图像
                    name, msg = _ocr_single_box(
                        ocr_img,
                        self.ocr_lang
                    )

                    # 如果识别到有效的消息或姓名，则加入结果列表
                    if msg or name:
                        pairs.append((name, msg))

                    # 收集预览图像用于调试显示
                    cleaned_previews.append(ocr_img)  # 清理后的灰度图

                # ==================== 第5步：帧级去重处理 ====================
                # 将识别结果标准化为元组，用于检测帧间变化
                # 空值统一转为空字符串，避免None和""的差异导致误判
                norm_pairs = tuple((name or "", msg or "") for name, msg in pairs)

                # 检查当前帧的识别结果是否与上一帧不同
                frame_changed = (norm_pairs != self._last_frame_signature)

                # ==================== 第6步：调试预览显示 ====================
                # 如果启用了调试预览功能，生成三个阶段图像（分别提供给前端）
                if self.debug_preview:
                    # RAW阶段：在原始图像上绘制检测到的消息框（红色边框）
                    raw_stage = _draw_boxes_on_image(img_raw, boxes)

                    # CLEANED阶段：将所有框的清理后图像垂直堆叠；若无框则用原图灰度
                    cleaned_stage = _stack_images_vertical(cleaned_previews)

                    # 分别保存最近一帧各阶段图像，供 Web 获取
                    try:
                        with self._last_preview_lock:
                            self._last_raw_image = raw_stage.copy()
                            self._last_cleaned_image = cleaned_stage.copy()
                    except Exception:
                        pass

                # ==================== 第8步：关键词匹配和结果通知 ====================
                hits: List[Tuple[str, str]] = []  # 存储关键词命中的消息

                if pairs:  # 如果识别到了消息内容
                    now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")

                    # 如果配置了关键词列表，进行关键词匹配
                    if self.keywords:
                        for name, msg in pairs:
                            lm = (msg or "").lower()  # 将消息转为小写进行匹配
                            # 检查消息中是否包含任何关键词
                            if any(kw in lm for kw in self.keywords):
                                hits.append((name, msg))  # 记录命中的消息

                    # 只有在帧内容发生变化时才通知监听器，实现去重
                    if frame_changed:
                        self._notify_listeners(now, pairs, hits)
                        self._last_frame_signature = norm_pairs  # 更新帧签名
                else:
                    # 如果没有识别到消息，但帧状态发生了变化（比如从有消息变为无消息）
                    if frame_changed:
                        now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                        self._notify_listeners(now, [], [])  # 通知监听器状态变化
                        self._last_frame_signature = norm_pairs

            except Exception as e:
                # ==================== 异常处理 ====================
                print(f"[消息监控] 发生错误: {e}")

            finally:
                # ==================== 循环控制 ====================
                time.sleep(self.interval_sec)  # 按配置的间隔等待
